查看原文
其他

大量实例助攻,让你的单元测试更高效

无欢 搜狐技术产品 2021-07-27

本文字数:4780

预计阅读时间:15分钟


导读

单元测试作为程序员的必修课,对代码的稳定性起着关键性的作用,但是你真的会写单元测试么?什么才算是真正的单元测试?这些疑问你都将在文章中得到解答。


在本文中,我们将主要基于Mockito框架来介绍如何编写单元测试,必要时使用PowerMock来对一些Mockito无法处理的方法进行操作,并且伴随有大量实例以助于理解。


1  什么是单元测试

什么是单元测试?我们先看看维基百科中对其的定义:

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。


在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

根据以上定义,简单来说Java中单元测试就是对类中方法进行测试的工作。

2  单元测试的意义

2.1 为什么要进行单元测试

单元测试是软件测试的基础,不仅会直接影响到软件的后期测试,在很大程度上还会影响产品的最终质量。而且能写出高质量的单元测试代码,也是程序猿的基本修养之一。


提高代码质量,写过单元测试的代码,在提测、联调时bug数会明显减少,修改代码时也会对代码也会更有信心;

减少调试时间,如果认真的做好了单元测试,在系统集成联调时非常顺利,减少查找、调试bug时反复编译的时间成本;

在单元测试时某些问题就很容易发现,而在后期的测试中发现问题所花的成本将成倍数上升。

为代码重构保驾护航,可以更放心的去重构代码;

通过单元测试快速熟悉代码,比如代码做什么工作,有哪些特殊情况需要考虑,包含哪些业务。


2.2 什么时候写单元测试

编写单元测试的时机无非以下三种:

代码实现之前(TDD提倡);

与代码实现同步进行,开始写之前想好细节用例,然后一个个实现,代码一步步完善;

代码完成之后再写单元测试,效果不如前2种,而且单元测试的难度和工作量可能随着代码质量的变化成倍增加。


很多人觉得单元测试是在代码完成之后编写的,其实这种想法是错误的,根据TDD(Test-driven development)思想,倡导先写测试程序,然后再具体实现其功能。


但是很多人可能没有达到这种水平,此时退而求其次,边写代码边完成单元测试不妨为一种折中的选择。



2.3 哪些情况需要写单元测试

那么单元测试的粒度需要划分多细,哪些情况才需要写单元测试呢?以下四点可供参考:

涉及大量计算;公共代码、工具类;

逻辑复杂、容易出错、不易理解;

核心业务代码。


3  单元测试的方法

在Sprig Boot环境下进行单元测试,首先需要在pom.xml中添加包依赖:


1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-test</artifactId>
4    <scope>test</scope>
5</dependency>


Spring Boot提供的spring-boot-starter-test启动器集成了常用的测试类库:



Spring Boot中单元测试类放在src/test/java目录下,通过IDEA可以自动创建测试类,快捷键为==⇧⌘T==(MAC)或者==Ctrl+Shift+T==(Window)。


还可以通过添加Coverage插件分析测试覆盖率。


3.1 Mock介绍

在写单元测试的过程中,一个很普遍的问题是,要测试的类可能会有很多的依赖,而这些依赖的类、对象又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。


基于以上问题,我们引入了Mock方法,对被测试类所依赖的其他类和对象进行mock——构建它们的一个假对象,并定义这些假对象的行为,然后提供给被测试对象使用。


被测试对象像使用真的对象一样使用它们,这样我们就可以把测试的目标限定于被测试对象本身,从而实现依赖的隔离。


简单来说,mock对象就是在调试期间用来作为真实对象的替代品;mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法。


3.2 使用的框架

Mockito是一个针对Java的单元测试模拟框架,是为了简化单元测试过程中测试上下文(或者称之为测试驱动函数以及桩函数)的搭建而开发的工具。


3.3 测试流程

1@RunWith(MockitoJUnitRunner.class)


在被测试类之前加以上注解,表示使用指定运行器来运行测试类。MockitoJUnitRunner不需加载其他spring bean,也不需要启动spring的那一整套东西,启动速度非常快。



3.3.1 创建mock对象

对被测类中@Autowired的对象,用@Mock标注;

对被测类自己,用@InjectMocks标注。

被@Mock标注的对象会自动注入到被@InjectMocks标注的对象中。

@Spy可以实现部分mock,即不打桩时默认会执行真实的方法,如果打桩则返回桩实现。下面是官方给出的一个示例:


1   List list = new LinkedList();
2   List spy = spy(list);
3
4   //optionally, you can stub out some methods:
5   when(spy.size()).thenReturn(100);
6
7   //using the spy calls real methods
8   spy.add("one");
9   spy.add("two");
10
11   //prints "one" - 这个函数还是真实的
12   System.out.println(spy.get(0));
13
14   //100 is printed - size()函数被替换了
15   System.out.println(spy.size());



3.3.2 设置测试桩

也叫打桩,就是定制mock对象的具体行为,通过它可以指定某个类的某个方法在什么情况下返回什么样的值。


org.mockito.Mockito:

when(...).thenReturn(...)

doReturn(...). when(class).method(...)

大多情况下,上面2种方法通用,建议优先使用when-thenReturn,因为其可读性较高且会检查回传值的类型(Type-Safe Check):


1        List<String> list = mock(List.class);
2        when(list.get(100)).thenReturn("33");   //返回字符串,正常
3        when(list.get(0)).thenReturn(33);     //返回数字,IDE报错,进行了type-safety的检查
4        doReturn(33).when(list).get(0); //IDE没报错,没进行type-safety的检查


并且对于Spy对象,when-thenReturn会真实调用方法,只是返回时返回设定的值,而doReturn根本不会调用实际的方法:


1    public boolean exist(String telOrigin) {
2        int i = 1/0;
3        return Optional.ofNullable(mapper.selectByTelOrigin(telOrigin)).isPresent();
4    }


1    @Test
2    @Rollback
3    public void save() {
4        BusinessAgent businessAgent = new BusinessAgent();
5        businessAgent.setTelOrigin("13888888888");
6        businessAgent.setAgentId(1);
7        doReturn(true).when(businessAgentRepository).exist("13888888888");
8//        when(businessAgentRepository.exist("13888888888")).thenReturn(true);
9        int agentId = businessAgentRepository.save(businessAgent);
10        verify(businessAgentExtMapper).updateByPrimaryKeySelective(businessAgent);
11        verify(businessAgentExtMapper, never()).insert(businessAgent);
12        assertThat(agentId, equalTo(businessAgent.getAgentId()));
13        ……
14    }


比如上述代码,对于被测试类中存在1/0的非法算数运算,运行时汇报异常,使用when-thenReturn时测试无法通过,报算数异常,但是doReturn时测试可以正常通过,说明其根本没有调用实际方法。


org.mockito.BDDMockito:

given(...).willReturn(...)

更符合BDD开发的习惯,Given…When…Then…实际上就是设定场景的状态、适用的事件,以及场景的执行结果,如:


1given(businessHousePic.getHouseId()).willReturn(3L);



3.3.3 调用方法

调用被测对象的方法,获取返回值。



3.3.4 验证结果

对有返回值的方法,验证返回数据是否和期望匹配。这里用到了2个方法verify()和assertThat():


1/**验证businessHousePicRepository调用了方法invalid(url)一次*/
2verify(businessHousePicRepository).invalid(url);
3/**验证businessAgentExtMapper没有执行方法insert(businessAgent)*/
4verify(businessAgentExtMappernever()).insert(businessAgent);
5/**验证结果相等。assertThat方法将再下一节详细介绍*/
6assertThat(agentIdequalTo(businessAgent.getAgentId()));


对无返回值的方法可以验证方法调用次数,也可以用:doNothing().when(mock.someMethod())。



3.3.5 assertThat断言

assertThat是JUnit 4.4引入的一个全新的断言语法,可以只使用这一个方法,实现所有的断言测试。assertThat语法如下:


1assertThat(T actual, Matcher<? super T> matcher);


其中actual为需要测试的变量值,matcher为使用Hamcrest的匹配符来表达变量actual期望值的声明,如果value值与matcher所表达的期望值相符,则测试成功,否则失败。


这里提到的Hamcrest也是JUnit 4.4引入的一个框架,它提供了一套完整的匹配符Matcher,这些匹配符更接近自然语言,可读性高,更加灵活。当然也可以自己定义设计匹配符,这个可以自行参考相关资料。

一些常用的匹配器如下:


核心

anything - 总是匹配

describedAs - 装饰器,添加自定义失败描述

is - 装饰器,用以提高可读性


逻辑

allOf - 如果所有匹配器匹配则匹配,短路(如java中的&&)

anyOf - 如果任何匹配器匹配则匹配,短路(如java中的||)

not - 如果匹配器不匹配则匹配,反之亦然


对象

equalTo - 使用Object.equals测试对象相等

hasToString - 测试Object.toString

instanceOf,isCompatibleType - 测试类型

notNullValue,nullValue - null测试

sameInstance - 测试对象标示


Bean

hasProperty - 测试JavaBeans属性


集合

array - 针对数组的匹配器,测试数组元素

hasEntry, hasKey, hasValue - 测试Map的entry,key,value

hasItem, hasItems - 测试一个集合包含的元素

hasItemInArray - 测试一个数组中包含的一个元素


Number

closeTo - 测试浮点值接近给定值

greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo - 测试大小排序


文本

equalToIgnoringCase - 测试字符串忽略大小写等式

equalToIgnoringWhiteSpace - 测试字符串等式忽略大小写和空白符

containsString, endsWith, startsWith - 测试字符串匹配


一些常用的实例如下:


1// equalTo可以断言数值、字符串和对象是否相等,相当于Object的equals方法
2// 可以使用equalToIgnoringCase()进行字符串之间忽略大小写的匹配
3assertThat(testedObject, equalTo(expectedObject));
4// is匹配符断言被测的object等于后面给出匹配表达式,not正好相反
5assertThat(testedObject, is(equalTo(expectedObject)));
6// is匹配符简写应用之一,is(equalTo(x))可简写为
7assertThat(testedObject, is(expectedObject));
8// is匹配符简写应用之二,is(instanceOf(SomeClass.class))断言testedObject为Test的实例
9assertThat(testedObject, is(Test.class));
10// containsString断言被测的字符串testedString包含子字符串subString
11assertThat(testedString, containsString(subString));
12// startsWith匹配符断言被测的字符串testedString以子字符串prefix开始
13// endsWith真好相反,判断以某字符串结尾
14assertThat(testedString, startsWith(prefix));
15// nullValue()匹配符断言被测object的值为null,notNullValue()相反
16assertThat(object, nullValue());
17// closeTo匹配符断言被测的浮点型数testedDouble在20.0±0.5范围之内
18// 类似的比较数值大小的匹配符有lessThan,lessThanOrEqualTo,greaterThan等
19assertThat(testedDouble, closeTo(20.00.5));
20// hasItem匹配符表明被测的迭代对象iterableObject含有元素element
21// 类似的集合匹配符还有hasEntry,hasKey,hasValue等
22assertThat(iterableObject, hasItem (element));
23// allOf匹配符断言符合所有条件,相当于“与”(&&)
24// anyOf匹配符断言符合条件之一,相当于“或”(||)
25assertThat(testedNumber, allOf(greaterThan(8), lessThan(16)));


equalTo重写了equals方法,is是一个匹配器的包装器,使用is(T value)时会被重载为is(equalTo(value))返回。


使用assertThat的好处有:


替代之前诸多的assertion语句,所有的单元测试中只使用一个断言方法,使得代码风格变得统一,更容易维护;


assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(he, is("a pig"));),使得代码更加直观、易读;


assertThat使用了Hamcrest的Matcher匹配符,具有很强的易读性,而且使用起来更加灵活,当匹配条件比较复杂时体会更加明显。而且匹配符可以联合起来使用,满足复杂的测试要求;


例如:


1    // 联合匹配符anyOf和containsString表示“包含任何一个子字符串”
2    assertThat(something, anyOf(containsString("dev"), containsString("test")));


错误信息更加易懂,以前代码出错只会抛出一些比较笼统的错误信息,比如,junit.framework.AssertionFailedError:null。


使用assertThat后会描述一些比较详细的错误信息,比如:


1    String s = "hello world!"
2    assertThat( s, anyOf( containsString("dev"), containsString("test") ) ); 
3    // 如果出错后,系统会自动抛出以下提示信息:
4    java.lang.AssertionError: 
5    Expected: (a string containing "dev" or a string containing "test"
6    got: "hello world!"


3.4 单元测试举例


3.4.1 DAO层单元测试

1    @ResultType(DynamicInfo.class)
2    @Select("select id from dynamic_info where crawl_id=#{0} and status=1 and source=#{1}")
3    DynamicInfo getDynamicByCrawlId(long crawlId, int source);


对上面sql语句单元测试有下面几种方法:

连接一个真实的数据库;

选择内存数据库模拟真实数据库;

创建Mock对象,在测试时模拟getDynamicByCrawlId方法。

第一种方法不能完全算是单元测试,因为他完全依赖数据库,第二种可以实现单元测试,但是比较复杂,第三种只是对DAO层的模拟,但是这对上面的sql语句其实没有任何作用,即使这个测试通过,它甚至不能保证这个DAO所执行的 SQL能在数据库中正确执行。


下面时使用第三种方法时,使用Mockito可以这么测:


1@RunWith(MockitoJUnitRunner.class)
2@Transactional
3public class BusinessAgentRepositoryTest {
4
5    @Mock
6    private BusinessAgentExtMapper businessAgentExtMapper;
7
8    @InjectMocks
9    @Spy
10    private BusinessAgentRepository businessAgentRepository;
11
12    @Test
13    @Rollback
14    public void save() {
15        BusinessAgent businessAgent = new BusinessAgent();
16        businessAgent.setTelOrigin("13888888888");
17        businessAgent.setAgentId(1);
18        doReturn(true).when(businessAgentRepository).exist("13888888888");
19        int agentId = businessAgentRepository.save(businessAgent);
20        verify(businessAgentExtMapper).updateByPrimaryKeySelective(businessAgent);
21        verify(businessAgentExtMapper, never()).insert(businessAgent);
22        assertThat(agentId, equalTo(businessAgent.getAgentId()));
23
24        BusinessAgent businessAgent2 = new BusinessAgent();
25        businessAgent2.setTelOrigin("13888888887");
26        businessAgent2.setAgentId(2);
27        doReturn(false).when(businessAgentRepository).exist("13888888887");
28        int agentId2 = businessAgentRepository.save(businessAgent2);
29        verify(businessAgentExtMapper, never()).updateByPrimaryKeySelective(businessAgent2);
30        verify(businessAgentExtMapper).insert(businessAgent2);
31        assertThat(agentId2, equalTo(businessAgent2.getAgentId()));
32    }
33}


DAO层跟数据库耦合的是如此的紧密,以至于脱离数据库的测试没有一点意义。


其实我们对 DAO 层的测试是为了两点:

它执行了正确的 SQL 吗?

它返回了我们所需要的数据吗?


这两点都是需要数据库环境支持的。但是如果直接连接数据库又会使得测试过于依赖运行的环境,如果其他人在同时开发这个项目,他也需要搭建测试用的数据库,这很麻烦。而且这样的测试已经不能叫单元测试了,更像是集成测试。


也有方法能实现模拟数据库,比如用内存数据库(H2、HSQLDB等)来解决该问题。引入内存数据库之后需要在代码中管理ddl脚本和必要的初始化数据dml脚本,每次跑单元测试时启动内存数据库,刷ddl和dml脚本,然后执行单元测试逻辑。管理数据库脚本工具有flyway和liquibase等。但是这些数据库的语法、特性很可能和自己使用的数据库不同,从而导致测试失败。


笔者更倾向于将DAO层的测试在集成测试的时候一起测,配合Transactional和RollBack注解保证对数据库不产生影响。例如:


1@RunWith(SpringJUnit4ClassRunner.class)    
2@ContextConfiguration(locations = {"classpath:spring.xml"})    
3@Transactional    
4@Rollback(true)    
5public class StudentServiceTest {    
6
7    @Autowired    
8    private StudentService studentService;    
9
10    @Test    
11    public void testInsertStudent() {    
12        Student s = new Student("test""male", (byte23"110");    
13        studentService.insertStudent(s);    
14        Assert.assertEquals(studentService.getStudentsById(s.getId()).getName(),"test");    
15        Assert.assertEquals(studentService.getStudentsById(s.getId()).getAge().intValue(), 23);    
16    }    
17
18    @Test    
19    public void testUpdateStudent() {    
20        Student s = new Student("test""male", (byte23"110");    
21        studentService.insertStudent(s);    
22        Assert.assertEquals(studentService.getStudentsById(s.getId()).getName(),"test");    
23        Assert.assertEquals(studentService.getStudentsById(s.getId()).getAge().intValue(), 23);    
24
25        s.setAge((byte)25);    
26        s.setName("test2");    
27        studentService.updateStudent(s);    
28        Assert.assertEquals(studentService.getStudentsById(s.getId()).getName(),"test2");    
29        Assert.assertEquals(studentService.getStudentsById(s.getId()).getAge().intValue(), 25);    
30    } 
31}




3.4.2 对Servicervice层测试

3.4.2.1 Service层单元测试


1    /**
2     * @Description: 按照资讯通过率筛选用户
3     * @param rate 资讯通过率
4     */

5    @ErrorHandler
6    public Resp getUserNewsStatisticsByRate(double rate) {
7
8        if (rate < 0 || rate > 100) {
9            return RespUtil.fail(CodeEnum.PARAM_ILLEGAL.getCode(), CodeEnum.PARAM_ILLEGAL.getMsg());
10        }
11
12        List<UserNewsStatisticsVO> userNewsStatisticsList = newsDAO.getUserNewsStatistics();
13        if (CollectionUtils.isEmpty(userNewsStatisticsList)) {
14            return RespUtil.fail(CodeEnum.DATA_NULL.getCode(), CodeEnum.DATA_NULL.getMsg());
15        }
16
17        List<UserNewsStatisticsVO> results = Lists.newLinkedList();
18        for (UserNewsStatisticsVO userNewsStatistics : userNewsStatisticsList) {
19            userNewsStatistics.calRate();
20            UserAccountVO userAccount = userVestService.getUserAccountByUid(userNewsStatistics.getUid());
21            if(userAccount != null){
22                userNewsStatistics.setPublisher(userAccount.getAccountName());
23            }
24            if (userNewsStatistics.getRate() >= rate) {
25                results.add(userNewsStatistics);
26            }
27        }
28        results.sort(Comparator.comparing(UserNewsStatisticsVO::getRate).thenComparing(UserNewsStatisticsVO::getOkCount).reversed());
29        return RespUtil.ok(results);
30    }


上面是一个统计用户发文数据的方法,下面是对他的单元测试:


1@RunWith(MockitoJUnitRunner.class)
2@Slf4j
3public class CountServiceTest {
4    @Mock
5    private NewsDAO newsDAO;
6
7    @Mock
8    private UserVestService userVestService;
9
10    @InjectMocks
11    private CountService countService;
12
13    @Test
14    public void getUserNewsStatisticsByRate() {
15        //边界条件
16        Resp fail = new Resp();
17        fail.setCode(CodeEnum.PARAM_ILLEGAL.getCode());
18        fail.setMsg(CodeEnum.PARAM_ILLEGAL.getMsg());
19
20        assertThat(countService.getUserNewsStatisticsByRate(101),equalTo(fail));
21        assertThat(countService.getUserNewsStatisticsByRate(-11),equalTo(fail));
22
23        //返回数据为空
24        Resp fail2 = new Resp();
25        fail2.setCode(CodeEnum.DATA_NULL.getCode());
26        fail2.setMsg(CodeEnum.DATA_NULL.getMsg());
27        given(newsDAO.getUserNewsStatistics()).willReturn(null);
28        assertThat(countService.getUserNewsStatisticsByRate(80), equalTo(fail2));
29
30        //正常情况
31        UserAccountVO userAccount2 = new UserAccountVO();
32        userAccount2.setUid(2L);
33        userAccount2.setAccountName("sohu");
34
35        UserAccountVO userAccount3 = new UserAccountVO();
36        userAccount3.setUid(3L);
37        userAccount3.setAccountName("focus");
38
39        UserNewsStatisticsVO user1 = new UserNewsStatisticsVO();
40        user1.setUid(1);
41        user1.setOkCount(2);
42        user1.setRejectCount(1);
43        user1.setDeleteCount(1);
44
45        UserNewsStatisticsVO user2 = new UserNewsStatisticsVO();
46        user2.setUid(2);
47        user2.setOkCount(3);
48        user2.setRejectCount(1);
49        user2.setDeleteCount(1);
50
51        UserNewsStatisticsVO user3 = new UserNewsStatisticsVO();
52        user3.setUid(3);
53        user3.setOkCount(6);
54        user3.setRejectCount(2);
55        user3.setDeleteCount(2);
56
57        UserNewsStatisticsVO user4 = new UserNewsStatisticsVO();
58        user4.setUid(4);
59        user4.setOkCount(16);
60        user4.setRejectCount(2);
61        user4.setDeleteCount(2);
62
63        List<UserNewsStatisticsVO> list = Lists.newArrayList(user1,user2, user3, user4);
64        given(newsDAO.getUserNewsStatistics()).willReturn(list);
65        given(userVestService.getUserAccountByUid(1)).willReturn(null);
66        given(userVestService.getUserAccountByUid(2)).willReturn(userAccount2);
67        given(userVestService.getUserAccountByUid(3)).willReturn(userAccount3);
68        given(userVestService.getUserAccountByUid(4)).willReturn(null);
69
70        user2.setPublisher(userAccount2.getAccountName());
71        user3.setPublisher(userAccount3.getAccountName());
72
73        //测试用例1—通过率50%
74        Resp ok_50 = new Resp();
75        ok_50.setCode(CodeEnum.OK.getCode());
76        ok_50.setMsg(CodeEnum.OK.getMsg());
77        ok_50.setData(Lists.newArrayList(user4, user3, user2, user1));
78        assertThat(countService.getUserNewsStatisticsByRate(50),equalTo(ok_50));
79
80        //测试用例2—通过率55%
81        Resp ok_55 = new Resp();
82        ok_55.setCode(CodeEnum.OK.getCode());
83        ok_55.setMsg(CodeEnum.OK.getMsg());
84        ok_55.setData(Lists.newArrayList(user4, user3, user2));
85        assertThat(countService.getUserNewsStatisticsByRate(55),equalTo(ok_55));
86
87        //测试用例3—通过率70%
88        Resp ok_70 = new Resp();
89        ok_70.setCode(CodeEnum.OK.getCode());
90        ok_70.setMsg(CodeEnum.OK.getMsg());
91        ok_70.setData(Lists.newArrayList(user4));
92        assertThat(countService.getUserNewsStatisticsByRate(70),equalTo(ok_70));
93
94        //测试用例4—通过率100%
95        Resp ok = new Resp();
96        ok.setCode(CodeEnum.OK.getCode());
97        ok.setMsg(CodeEnum.OK.getMsg());
98        ok.setData(Lists.newArrayList());
99        assertThat(countService.getUserNewsStatisticsByRate(100),equalTo(ok));
100
101    }
102}


3.4.2.2 异常测试


try...fail...catch...

推荐使用这种方式,符合一般编程习惯,当没有异常被抛出的时候fail方法会被调用,输出测试失败的信息。而且在捕获到异常后不会跳出当前test function,之后操作依然会被顺序执行。


1    @Test
2    public void testDivisionWithException() 
{
3        try {
4            int i = 1 / 0;
5            fail(); 
6        } catch (ArithmeticException e) {
7            assertThat(e.getMessage(), is("/ by zero"));
8        }
9    }
10
11    @Test
12    public void testEmptyList() 
{
13        try {
14            new ArrayList<>().get(0);
15            fail("Expected an IndexOutOfBoundsException to be thrown");
16        } catch (IndexOutOfBoundsException e) {
17            assertThat(e.getMessage(), is("Index: 0, Size: 0"));
18        }
19
20        System.out.println("test");  //会被执行,打印test
21    }


@Test(expected=xxx)

问题:当被标记的这个测试方法中的任何一个操作抛出了相应的异常时,这个测试就会通过,而且没法同时测试多个异常。


1    /**
2     * 如果测试该方法时产生一个ArithmeticException的异常,则表示测试通过
3     * 你可以改成int i = 1 / 1;运行时则会测试不通过-因为与你的期望的不符
4     */

5    @Test(expected = ArithmeticException.class)
6    public void testDivisionWithException() {
7        int i = 1 / 0;
8        System.out.println("test");  //不会被执行
9    }
10
11    /**
12     * 运行时抛出一个IndexOutOfBoundsException异常才会测试通过
13     */

14    @Test(expected = IndexOutOfBoundsException.class)
15    public void testEmptyList() {
16        new ArrayList<>().get(0);
17    }


ExpectedException Rule

JUnit 4.7之后才有的,可以测试到异常类型和异常信息。它需要在测试之前使用Rule标记来指定一个ExpectedException,并在测试相应操作之前指定期望的Exception类型。


问题:ExpectedException进行异常测试后,当前的test function就结束了,如果在expect的相应操作之后还有assert的话会被自动跳过


1    @Rule
2    public ExpectedException thrown = ExpectedException.none();
3
4    @Test
5    public void testDivisionWithException() {
6
7        thrown.expect(ArithmeticException.class);
8        thrown.expectMessage(containsString("/ by zero"));
9
10        int i = 1 / 0;
11
12        System.out.println("test");  //不会被执行
13    }


3.4.2.3 类中方法相互调用

类中方法相互调用的时候,如果要对其中一个方法进行单元测试,这种情况下并没有对被测类进行mock,如果想要对该方法调用的类内方法进行mock的话,需要用到spy。


被测类:


1@Transactional
2@Repository
3public class BusinessAgentRepository {
4    private final BusinessAgentExtMapper mapper;
5
6    @Autowired
7    public BusinessAgentRepository(BusinessAgentExtMapper mapper) {
8        this.mapper = mapper;
9    }
10
11    public boolean exist(String telOrigin) {
12        return Optional.ofNullable(mapper.selectByTelOrigin(telOrigin)).isPresent();
13    }
14
15    public int save(BusinessAgent businessAgent) {
16        if (exist(businessAgent.getTelOrigin())) {
17            mapper.updateByPrimaryKeySelective(businessAgent);
18        }
19        else {
20            mapper.insert(businessAgent);
21        }
22        return businessAgent.getAgentId();
23    }
24}


测试类:


1@RunWith(MockitoJUnitRunner.class)
2@Transactional
3public class BusinessAgentRepositoryTest {
4
5    @Mock
6    private BusinessAgentExtMapper businessAgentExtMapper;
7
8    @InjectMocks
9    @Spy
10    @Autowired
11    private BusinessAgentRepository businessAgentRepository;
12
13    @Test
14    public void exist() { … }    
15
16    @Test
17    @Rollback
18    public void save() {
19        BusinessAgent businessAgent = new BusinessAgent();
20        businessAgent.setTelOrigin("13888888888");
21        businessAgent.setAgentId(1);
22//        when(businessAgentRepository.exist("13888888888")).thenReturn(true);
23        doReturn(true).when(businessAgentRepository).exist("13888888888");
24        int agentId = businessAgentRepository.save(businessAgent);
25        verify(businessAgentExtMapper).updateByPrimaryKeySelective(businessAgent);
26        verify(businessAgentExtMapper, never()).insert(businessAgent);
27        assertThat(agentId, equalTo(businessAgent.getAgentId()));
28
29        BusinessAgent businessAgent2 = new BusinessAgent();
30        businessAgent2.setTelOrigin("13888888887");
31        businessAgent2.setAgentId(2);
32        doReturn(false).when(businessAgentRepository).exist("13888888887");
33        int agentId2 = businessAgentRepository.save(businessAgent2);
34        verify(businessAgentExtMapper, never()).updateByPrimaryKeySelective(businessAgent2);
35        verify(businessAgentExtMapper).insert(businessAgent2);
36        assertThat(agentId2, equalTo(businessAgent2.getAgentId()));
37    }
38}



3.4.3 对Controller层测试

Controller层已经到到接口端了,而且大部分逻辑都写在service层,很多情况可以在集成测试时测。


但是对于一些项目可能会在controller层进行比较多的处理,比如存放字段、页面重定向等操作,或者当设置了过滤器、拦截器时,有鉴权等服务时,这时有必要对Controller层进行单元测试。


对Controller层(API)做测试,需要用到MockMvc,可以不必启动工程就能测试这些接口。MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。


1@RunWith(SpringRunner.class)
2@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
3@AutoConfigureMockMvc
4@Transactional
5public class InvalidPicControllerTest {
6
7    @Autowired
8    private WebApplicationContext webApplicationContext;
9
10    @Autowired
11    private MockMvc mockMvc;
12
13    @MockBean
14    private InvalidPicService invalidPicService;
15
16    private static String str = "ok";
17
18    @Before
19    public void setUp(){
20        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
21    }
22
23    @Test()
24    public void invalidHousePicByUrl() throws Exception{
25        String picUrl = "111";
26        String name = "url" ;
27        mockMvc.perform(MockMvcRequestBuilders.get("/invalidHousePicByUrl")
28                .param(name, picUrl)
29                .accept(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
30        )
31                .andExpect(MockMvcResultMatchers.status().isOk())
32                .andExpect(MockMvcResultMatchers.content().string(str))
33                .andDo(MockMvcResultHandlers.print());
34        verify(invalidPicService).invalidHousePic(picUrl);
35    }
36}


Perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理

MockMvcRequestBuilders.get():构造一个请求,如getget("/invalidHousePicByUrl"),同理Post等其他方法

contentType(MediaType mediaType):指定请求的contentType头信息,例如MediaType.APPLICATION_JSON_UTF8代表发送端发送的数据格式是application/json;charset=UTF-8

accept(MediaType mediaTypes):指定请求的Accept头信息

session(session):注入一个session,当有拦截器时可以用来通过验证

andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否是预期的

andExpect(MockMvcResultMatchers.status().isOk()):查看请求的状态响应码是否为200,如果不是则抛异常,测试不通过

andExpect(MockMvcResultMatchers.content().string(str)):获取请求的响应内容是否为“ok”,不是就测试不通过

ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print()打印整个响应结果信息

andReturn() :返回验证成功后的MvcResult,用于自定义验证/下一步的异步处理



3.4.5 powermock测试特殊类、对象

PowerMock也是一个单元测试模拟框架,它在其它单元测试模拟框架(如Mockito和EasyMock)的基础上做出了一些扩展,从而提供了更加强大的功能,能处理更多一般框架无法处理的情况,如对静态方法、构造方法和私有方法等方法的模拟。


PowerMock在扩展功能时完全采用和原框架相同的 API, 非常容易上手。如果你已经熟悉了使用Mockito来编写单元测试,那么你会发现PowerMock学起来非常容易。


PowerMock的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。需要注意的是目前PowerMock仅支持 EasyMock 和 Mockito。


1<dependency>
2    <groupId>org.powermock</groupId>
3    <artifactId>powermock-module-junit4</artifactId>
4    <version>1.6.4</version>
5    <scope>test</scope>
6</dependency>
7<dependency>
8    <groupId>org.powermock</groupId>
9    <artifactId>powermock-api-mockito</artifactId>
10    <version>1.6.4</version>
11    <scope>test</scope>
12</dependency>


使用PowerMock时需要添加以上依赖,并在类开头处加上以下代码:


1// 用PowerMockerRunner来运行测试用例,否则无法使用PowerMock
2@RunWith(PowerMockRunner.class)
3// 所有需要测试的类,列在此处,以逗号分隔
4@PrepareForTest({UserController.class})


3.4.5.1 静态方法


1@RunWith(PowerMockRunner.class)
2@PrepareForTest(LocalStringUtils.class)
3@PowerMockIgnore("javax.management.*")
4public class CrawlToCheckConverterTest {
5
6    @Test
7    public void toBusinessProjectCheck() {
8        PowerMockito.mockStatic(LocalStringUtils.class);
9        PowerMockito.when(LocalStringUtils.removeHtmlLabel(null)).thenReturn("test");
10        assertThat(LocalStringUtils.removeHtmlLabel(null), is("test"));
11    }
12}


3.4.5.2 Mock私有方法

Mock私有方法时需要用到spy方法进行模拟,类似于类内方法互相调用情况。


1processHouseMessageService = PowerMockito.spy(processHouseMessageService);
2CrawlHouse crawlHouse = new CrawlHouse();
3crawlHouse.setUrl("url");
4crawlHouse.setHouseTitle("test");
5PowerMockito.when(processHouseMessageService, "checkMessageReady", crawlHouse).thenReturn(false);
6processHouseMessageService.checkCrawlMessage(crawlHouse);
7verify(businessProjectCheckMapper, never()).selectByUrl(anyString());


3.4.5.3 测试私有方法

WhiteBox这个工具类可以注入或者查看对象的私有属性及Invoke对象的方法(包括私有方法)


1// 方法一
2Method method = PowerMockito.method(ProcessHouseMessageService.class"checkMessageReady", CrawlHouse.class);
3boolean result = (boolean) method.invoke(processHouseMessageService, crawlHouse);
4assertThat(result, is(false));
5// 方法二
6result = Whitebox.invokeMethod(processHouseMessageService, "checkMessageReady", crawlHouse);
7assertThat(result, is(false));


3.4.5.4 构造方法

使被测代码中 new 操作返回的对象可以被随意定制,会很大程度的提高单元测试的效率。


1String filePath = "c:/Users";
2File mockFile = mock(File.class);
3PowerMockito.whenNew(File.class).withArguments(filePath).thenReturn(mockFile);
4when(mockFile.exists()).thenReturn(true);
5assertThat(mockFile.exists(), is(true));


3.4.5.5 Final方法

和普通方法一样mock即可。


3.4.5.6 Mock其他方法

返回值为 void 的 static 方法

泛型

可变参数

私有内部静态类对象

……


3.5 原理简单分析

Mock本质上是一个Proxy代理模式的应用,在代理对象调用方法前,用stub的方式设置其返回值,之后在真实调用时,通过代理对象返回之前预设的值。


在Mockito中通过when()方法设置条件时,Mockito并不是对内部实现不感知,相反,当条件中方法被调用的时候(调用的实际上是proxy对象mock的方法),Mockito保存了被调用的方法名,以及调用时候传递的参数,然后等到thenReturn()方法被调用的时候,再把需要的返回值保存起来,这样就有了构建一个stub方法所需的所有信息,就可以构建一个stub方法了。


PowerMock则通过提供定制的==类加载器==以及==字节码篡改==的方式,实现了对静态方法、构造方法、私有方法以及 Final 方法的模拟。


对于非系统类,PowerMock会创建一个MockClassLoader实例,用来加载被@PrepareForTest标注的类,然后根据不同mock需求修改被测类的class文件,以达到对静态方法、私有方法等方法等模拟实现。例如,通过去除final关键词来实现对final方法的mock。而对于系统类的final方法和静态方法,PowerMock会通过修改调用系统类的class文件,而不是直接修改系统类的class文件的方式,来满足mock需求。


参考资料:

[1]https://blog.csdn.net/vincetest/article/details/1378507

[2]http://tengj.top/2017/12/28/springboot12/

[3]http://thinkdevos.net/2017/09/29/2017-9-29-hamcrest/

[4]https://zhuanlan.zhihu.com/p/28983008

[5]https://github.com/powermock/powermock



也许你还想看

(▼点击文章标题或封面查看)

深度长文|循序渐进解读计算机中的时间—应用篇(上)

2019-11-14

深度长文|循序渐进解读计算机中的时间—应用篇(下)

2019-11-14

如何在Android中完成一个APT项目的开发?

2019-07-11


加入搜狐技术作者天团

千元稿费等你来!

戳这里!☛


     您对这篇文章有什么疑惑吗?欢迎留言讨论! ▼▼▼  

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存